天堂之门

天堂之门的实现基础

随着计算机发展,32位操作系统逐渐不能满足需求,64位操作系统出现,为了兼容旧有的32位程序,微软采用了WOW64作为解决办法。

WOW64

在64位的windows当中,64位程序和32位程序都可以正常运行,64位程序加载kernel32.dll(64位)和ntdll.dll(64位),而32位程序加载kernel32.dll(32位)和ntdll.dll(32位)。WOW64负责沟通两个版本的ntdll。换句话说,windows当中存在一个完整32位运行环境,依靠WOW64将程序调用的API转换到64位运行环境当中。

WOW64Test

在《逆向工程核心原理》中对WOW64进行了简要的介绍并提供了样例,现在查看这两个样例。

reverse-core-WOW64-1

这两个样例的源代码是一样的

#include "stdio.h"
#include "windows.h"
#include "Shlobj.h"
#include "tchar.h"
#pragma comment(lib, "Shell32.lib")

int _tmain(int argc, TCHAR* argv[])
{
    HKEY    hKey                = NULL;
    HANDLE  hFile               = INVALID_HANDLE_VALUE;
    TCHAR   szPath[MAX_PATH]    = {0,};
    ////////////////
    // system32 folder
    if( GetSystemDirectory(szPath, MAX_PATH) )
    {
        _tprintf(L"1) system32 path = %s\n", szPath);
    }
    ////////////////
    // File size
    _tcscat_s(szPath, L"\\kernel32.dll");
    hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL, 
                       OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if( hFile != INVALID_HANDLE_VALUE )
    {
        _tprintf(L"2) File size of \"%s\" = %d\n", 
        szPath, GetFileSize(hFile, NULL));
        CloseHandle(hFile);
    }
    ////////////////
    // Program Files
    if( SHGetSpecialFolderPath(NULL, szPath, 
                               CSIDL_PROGRAM_FILES, FALSE) )
    {
        _tprintf(L"3) Program Files path = %s\n", szPath);
    }
    ////////////////
    // Registry
    if( ERROR_SUCCESS == RegCreateKey(HKEY_LOCAL_MACHINE, 
                                      L"SOFTWARE\\ReverseCore", &hKey) )
    {
        RegCloseKey(hKey);
        _tprintf(L"4) Create Registry Key : HKLM\\SOFTWARE\\ReverseCore\n");
    }
    return 0;
}

不同的是x64版本被编译为64位程序,在64位操作系统下运行,而x86版本被编译为32位程序,通过WOW64在64位操作系统下运行。运行结果如下:

reverse-core-WOW64-2

发现32位程序的运行结果与64位程序结果不同,64位程序返回的是正确的值,而32位程序虽然识别的是C:\WINDOWS\system32\,但是实际指向内容却是转为32位程序准备的SysWOW64。这是WOW64对32位程序进行重定向的结果。

天堂之门实现原理

事实上,单纯的使用WOW64的32位程序在IDA中分析的结果是正常的,并不能反制逆向分析。真正的利用需要依靠CS段寄存器来实现。

windows依靠CS的值来判定程序是多少位,在32位程序当中CS值是0x23,在64位程序中CS的值是0x33。修改段寄存器之后32位程序会被当作64位程序来执行,这个转变足以欺骗静态分析和动态调试。而修改段寄存器之后,我们可在程序里放入需要的代码。

[CTFShow 月饼杯Ⅱ]EasyTea & [羊城杯 2021]OddCode

32位与64位混杂足以蒙骗IDA和其他调试器,先以两道题目为例体会一下该技术的应用,随后在详细了解该技术的实现细节。

[CTFShow 月饼杯Ⅱ]EasyTea

CTFSHOW-EasyTea-1

在jmp far ptr这条指令后面直接爆了一坨大的,实际上这条指令以及后面的错误的汇编指令是天堂之门的一项识别特征。jmp 指令后面跟着0x33,对CS段寄存器进行了修改,在之后又改回0x23执行32位程序。单独将这段程序框出来make function可以看到粗糙的伪代码。

CTFSHOW-EasyTea-2

通过这段代码可以找到密文(不知这里爆了非法内存访问是何用意),加密算法在另一个函数当中

int *__cdecl sub_401020(int *a1)
{
  int *result; // eax
  int j; // [esp+4Ch] [ebp-30h]
  int i; // [esp+50h] [ebp-2Ch]
  int v4; // [esp+54h] [ebp-28h]
  int v5[8]; // [esp+5Ch] [ebp-20h]

  v5[0] = 102;
  v5[1] = 108;
  v5[2] = 97;
  v5[3] = 103;
  v5[4] = 105;
  v5[5] = 115;
  v5[6] = 109;
  v5[7] = 101;
  result = a1;
  v4 = 0;
  for ( i = 0; i < 32; ++i )
  {
    v4 -= 2008542907;
    for ( j = 0; j < 7; ++j )
      a1[j] += (v5[v4 & 7] + v4) ^ (a1[j + 1] + ((a1[j + 1] >> 5) ^ (16 * a1[j + 1])));
    result = a1;
    a1[7] += (v5[v4 & 7] + v4) ^ (*a1 + ((*a1 >> 5) ^ (16 * *a1)));
  }
  return result;
}

是一个魔改Tea,可以得出解密脚本。

#include <stdio.h>
int map[8]={
0xB5ABA743, 0x4C5B3EE0, 0xB70AEB14, 0x6946BC13, 0x906089C4, 0x5B9F98F0, 0x0964B652, 0x78920976
};
void DeTea(int* v) {
    int sum=0;
    for(int i=0;i<32;i++)
    sum+=0x88481145;
    int key[] = { 0x66, 0x6c, 0x61, 0x67, 0x69, 0x73, 0x6d, 0x65 };
    unsigned int tmp = v[7];
    for (int j = 0; j < 32; j++) {
        v[7] -= (((v[0] << 4) ^ (v[0] >> 5)) + v[0]) ^ (sum + key[sum &7]);
        for (int i = 6; i >-1; i--) {
            v[i] -= (((v[i + 1] << 4) ^ (v[i + 1] >> 5)) + v[i + 1]) ^ (sum + key[sum & 7]);
        }
        
        sum -= 0x88481145;
    }    

}

int main(){

    DeTea(map);
    for(int i = 0;i<=7;i++){
        printf("%x\n",map[i]);
    } 
    return 0;
}
//5f616554
//34333231
//32315f35
//5f353433
//5f736579
//67616c66
//5f73695f
//79736165

输进去就拿flag

CTFSHOW-Easy_Tea-3

[羊城杯 2021]OddCode

羊城杯OddCode-1

可以看到加了很多的反分析的手段,直接把IDA橄榄了。尝试寻找一下修改CS的位置

羊城杯OddCode-2

sub_401010可以猜测为判定函数。一个可行的方法是将64位的sub_401010的内容单独提取出来作为程序分析。然而这个程序不止有天堂之门一个反分析手段,而是很多手段的大杂烩。从这个丑陋的状态来看,应该还有花指令和SMC之类的东西。暴力手搓并不明智,来看一看用模拟执行能整什么活。

unicorn

angr中有两个重要的部分,符号约束和模拟执行。符号约束基于z3实现,而模拟执行依靠类似于unicorn的各类模拟执行引擎实现

这里给出相关的链接汇编与反汇编神器Unicorn UnicornAPI文档

unicorn部分内容简介

1.虚拟内存:Unicorn 采用虚拟内存机制,使得虚拟CPU的内存与真实CPU的内存隔离。Unicorn 使用如下API来操作内存:

uc_mem_map
uc_mem_read
uc_mem_write

另外还有写寄存器的函数

reg_write

2.Hook 机制

Unicorn的Hook机制为编程控制虚拟CPU提供了便利。Unicorn 支持多种不同类型的Hook。大致可以分为(hook_add第一参数,Unicorn常量):

- 指令执行类
  - UC_HOOK_INTR
  - UC_HOOK_INSN
  - UC_HOOK_CODE
  - UC_HOOK_BLOCK
- 内存访问类
  - UC_HOOK_MEM_READ
  - UC_HOOK_MEM_WRITE
  - UC_HOOK_MEM_FETCH
  - UC_HOOK_MEM_READ_AFTER
  - UC_HOOK_MEM_PROT
  - UC_HOOK_MEM_FETCH_INVALID
  - UC_HOOK_MEM_INVALID
  - UC_HOOK_MEM_VALID
- 异常处理类
  - UC_HOOK_MEM_READ_UNMAPPED
  - UC_HOOK_MEM_WRITE_UNMAPPED
  - UC_HOOK_MEM_FETCH_UNMAPPED
针对题目的unicorn
from unicorn import *
from unicorn.x86_const import *
 
ADDRESS = 0x2E1000          # 程序加载的地址
INPUT_ADDRESS = 0x2E701D    # 输入的地址
KEY_ADDRESS = 0x2E705C      # 16字节key的地址
with open('OddCode.exe', 'rb') as file:
    file.seek(0x400)
    X64_CODE = file.read(0x4269)    # 读取代码
 
class Unidbg:
    def __init__(self, flag):
        mu = Uc(UC_ARCH_X86, UC_MODE_64)
        # 基址为0x2E1000,分配16MB内存
        mu.mem_map(ADDRESS, 0x1000000)
        mu.mem_write(ADDRESS, X64_CODE)
        mu.mem_write(INPUT_ADDRESS, flag)       # 随便写入一个flag
        mu.mem_write(KEY_ADDRESS, b'\x90\xF0\x70\x7C\x52\x05\x91\x90\xAA\xDA\x8F\xFA\x7B\xBC\x79\x4D')
        # 初始化寄存器,寄存器的状态就是切换到64位模式之前的状态,可以通过动调得到
        mu.reg_write(UC_X86_REG_RAX, 1)
        mu.reg_write(UC_X86_REG_RBX, 0x51902D)
        mu.reg_write(UC_X86_REG_RCX, 0xD86649D8)
        mu.reg_write(UC_X86_REG_RDX, 0x2E701C)
        mu.reg_write(UC_X86_REG_RSI, INPUT_ADDRESS)  # input参数
        mu.reg_write(UC_X86_REG_RDI, KEY_ADDRESS)    # key参数
        mu.reg_write(UC_X86_REG_RBP, 0x6FFBBC)
        mu.reg_write(UC_X86_REG_RSP, 0x6FFBAC)
        mu.reg_write(UC_X86_REG_RIP, 0x2E1010)
        mu.hook_add(UC_HOOK_CODE, self.trace)        # hook代码执行,保存代码块执行轨迹
        self.mu = mu
        self.except_addr = 0
        self.traces = []        # 用来保存代码块执行轨迹
 
    def trace(self, mu, address, size, data):
        if address != self.except_addr:
            self.traces.append(address)
        self.except_addr = address + size
    def start(self):
        try:
            self.mu.emu_start(0x2E1010, -1)
        except:
            pass
        print([hex(addr)for addr in self.traces])
Unidbg(b'SangFor{00000000000000000000000000000000}').start()

程序会输出所有进行代码块跳转的地址,另使用对内存读取hook输入和key的访问操作,来帮助我们找到访问输入和key的指令所在的代码块

mu.hook_add(UC_HOOK_MEM_READ, self.hook_mem_read)
 
def hook_mem_read(self, mu, access, address, size, value, data):
    if address >= INPUT_ADDRESS and address <= INPUT_ADDRESS + 41:
        print(f'Read input[{address - INPUT_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')
    if address >= KEY_ADDRESS and address <= KEY_ADDRESS + 16:
        print(f'Read key[{address - KEY_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')

后续的爆破flag的内容在对应的wp中。另外这道题还可以用windbg调试出运行逻辑之后得到flag,后续进行尝试

天堂之门实现

heaven-1

如图是一个常规的通过WoW64的API调用过程:

a.exe首先调用32位ntdll.dll中的ZwOpenProcess函数,接着ntdll32调用wow64cpu.dll中的X86SwitchTo64BitMode,调用该函数后进程从32位模式切换到64位模式。由wow64.dll将32位的系统调用转化为64位。最后调用64位ntdll.dll的ZwOpenProcess函数

Heaven-2

如图是使用了天堂之门技术的API调用过程:

  1. 跳转至64位模式
  2. 通过gs段选择子(gs:0x30)获取环境结构体(PEB64)
  3. 通过PEB64下的Ldr枚举所有加载的64位模块
  4. 定位NtDll64的基址(imageBase)
  5. 得到从DLL中获取导出APIntdll!LdrGetProcedureAddress
  6. BOOM! We got the key of Heaven’s Gate!
__asm {
		nop;
		_emit 0x6A;
		_emit 0x33;
		_emit 0xE8;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x83;
		_emit 0x04;
		_emit 0x24;
		_emit 0x05;
		_emit 0xCB;
	}
__asm {
		_emit 0xE8; 
		_emit 0x00;
		_emit 0x00; 
		_emit 0x00; 
		_emit 0x00;
		_emit 0xC7;
		_emit 0x44;
		_emit 0x24;
		_emit 0x04; 
		_emit 0x23; 
		_emit 0x00; 
		_emit 0x00; 
		_emit 0x00;
		_emit 0x83;
		_emit 0x04;
		_emit 0x24;
		_emit 0x0D;
		_emit 0xCB;
	}

参考链接:

ODDCODE题解

Unicorn使用介绍